﻿using System;
using System.Collections.Generic;
using System.Common.Lock;
using System.ComponentModel;
using System.Controls.Extensions;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace System.Controls
{
  public class TextBox : System.Windows.Controls.TextBox
  {
    #region ... Mask dependency properties ...

    public static readonly DependencyProperty CornerRadiusProperty =
      DependencyProperty.Register( "CornerRadius",
        typeof( CornerRadius ), typeof( TextBox ) );

    public static readonly DependencyProperty MaskProperty =
      DependencyProperty.Register( "Mask",
        typeof( string ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).ReserMaskProvider() ) );

    public static readonly DependencyProperty AsciiOnlyProperty =
      DependencyProperty.Register( "AsciiOnly",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).ReserMaskProvider() ) );

    public static readonly DependencyProperty IncludeLiteralsProperty =
      DependencyProperty.Register( "IncludeLiterals",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty AllowPromptAsInputProperty =
      DependencyProperty.Register( "AllowPromptAsInput",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).ReserMaskProvider() ) );

    public static readonly DependencyProperty IncludePromptProperty =
      DependencyProperty.Register( "IncludePrompt",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty IsPasswordProperty =
      DependencyProperty.Register( "IsPassword",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty PasswordCharProperty =
      DependencyProperty.Register( "PasswordChar",
        typeof( char ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty PromptCharProperty =
      DependencyProperty.Register( "PromptChar",
        typeof( char ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty ResetOnPromptProperty =
      DependencyProperty.Register( "ResetOnPrompt",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty ResetOnSpaceProperty =
      DependencyProperty.Register( "ResetOnSpace",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    public static readonly DependencyProperty SkipLiteralsProperty =
      DependencyProperty.Register( "SkipLiterals",
        typeof( bool ), typeof( TextBox ),
        new PropertyMetadata( ( sender, args ) => ( (TextBox) sender ).UpdateProviderParameters() ) );

    #endregion

    [DebuggerBrowsable( DebuggerBrowsableState.Never )]
    private readonly Suppressor fSuppressor = new Suppressor();

    [DebuggerBrowsable( DebuggerBrowsableState.Never )]
    private MaskedTextProvider fProvider;

    static TextBox()
    {
      DefaultStyleKeyProperty.OverrideMetadata( typeof( TextBox ),
        new FrameworkPropertyMetadata( typeof( TextBox ) ) );

      TextProperty.OverrideMetadata( typeof( TextBox ),
        new FrameworkPropertyMetadata(
          ( sender, args ) => ( (TextBox) sender ).TextChangedCallback(),
          ( sender, baseValue ) => ( (TextBox) sender ).CoerceTextCallback( baseValue ) ) );
    }

    private bool HasProvider
    {
      get { return !string.IsNullOrEmpty( Mask ); }
    }

    #region ... label ...

    public string SharedSizeGroup
    {
      get { return (string) GetValue( SharedSizeGroupProperty ); }
      set { SetValue( SharedSizeGroupProperty, value ); }
    }

    public static readonly DependencyProperty SharedSizeGroupProperty =
      DependencyProperty.Register( "SharedSizeGroup",
        typeof( string ), typeof( TextBox ),
        new PropertyMetadata( "TextBoxLabel" ) );

    public static readonly DependencyProperty ContentProperty =
      DependencyProperty.Register( "Content",
        typeof( object ), typeof( TextBox ),
        new PropertyMetadata( ContentChanged ) );

    public static readonly DependencyProperty HasContentProperty =
      DependencyProperty.Register( "HasContent",
        typeof( bool ), typeof( TextBox ) );

    public static readonly DependencyProperty ContentDockProperty =
      DependencyProperty.Register( "ContentDock",
        typeof( Dock ), typeof( TextBox ) );

    public object Content
    {
      get { return GetValue( ContentProperty ); }
      set { SetValue( ContentProperty, value ); }
    }

    public bool HasContent
    {
      get { return (bool) GetValue( HasContentProperty ); }
      set { SetValue( HasContentProperty, value ); }
    }

    public Dock ContentDock
    {
      get { return (Dock) GetValue( ContentDockProperty ); }
      set { SetValue( ContentDockProperty, value ); }
    }

    private static void ContentChanged( DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args )
    {
      var haslabel = args.NewValue != null;

      dependencyObject.SetValue( HasContentProperty, haslabel );
    }

    #endregion

    #region ... auto movefocus ...

    public static readonly DependencyProperty AutoMoveFocusProperty =
      DependencyProperty.Register( "AutoMoveFocus",
        typeof( bool ), typeof( TextBox ) );

    public static readonly RoutedEvent QueryMoveFocusEvent = EventManager.RegisterRoutedEvent( "QueryMoveFocus",
      RoutingStrategy.Bubble,
      typeof( QueryMoveFocusEventHandler ),
      typeof( TextBox ) );

    public bool AutoMoveFocus
    {
      get { return (bool) GetValue( AutoMoveFocusProperty ); }
      set { SetValue( AutoMoveFocusProperty, value ); }
    }

    #endregion

    #region ... mask properties ...

    public MaskedTextProvider Provider
    {
      get
      {
        EnsureProvider();
        return fProvider;
      }
    }

    public CornerRadius CornerRadius
    {
      get { return (CornerRadius) GetValue( CornerRadiusProperty ); }
      set { SetValue( CornerRadiusProperty, value ); }
    }

    public string Mask
    {
      get { return (string) GetValue( MaskProperty ); }
      set { SetValue( MaskProperty, value ); }
    }

    public bool AsciiOnly
    {
      get { return (bool) GetValue( AsciiOnlyProperty ); }
      set { SetValue( AsciiOnlyProperty, value ); }
    }

    public bool AllowPromptAsInput
    {
      get { return (bool) GetValue( AllowPromptAsInputProperty ); }
      set { SetValue( AllowPromptAsInputProperty, value ); }
    }

    public bool IncludeLiterals
    {
      get { return (bool) GetValue( IncludeLiteralsProperty ); }
      set { SetValue( IncludeLiteralsProperty, value ); }
    }

    public bool IncludePrompt
    {
      get { return (bool) GetValue( IncludePromptProperty ); }
      set { SetValue( IncludePromptProperty, value ); }
    }

    public bool IsPassword
    {
      get { return (bool) GetValue( IsPasswordProperty ); }
      set { SetValue( IsPasswordProperty, value ); }
    }

    public char PasswordChar
    {
      get { return (char) GetValue( PasswordCharProperty ); }
      set { SetValue( PasswordCharProperty, value ); }
    }

    public char PromptChar
    {
      get { return (char) GetValue( PromptCharProperty ); }
      set { SetValue( PromptCharProperty, value ); }
    }

    public bool ResetOnPrompt
    {
      get { return (bool) GetValue( ResetOnPromptProperty ); }
      set { SetValue( ResetOnPromptProperty, value ); }
    }

    public bool ResetOnSpace
    {
      get { return (bool) GetValue( ResetOnSpaceProperty ); }
      set { SetValue( ResetOnSpaceProperty, value ); }
    }

    public bool SkipLiterals
    {
      get { return (bool) GetValue( SkipLiteralsProperty ); }
      set { SetValue( SkipLiteralsProperty, value ); }
    }

    private void EnsureProvider()
    {
      if ( string.IsNullOrEmpty( Mask ) )
        return;

      fProvider = new MaskedTextProvider( Mask, CultureInfo.CurrentCulture, AllowPromptAsInput, PromptChar, PasswordChar, AsciiOnly );
    }

    private void ReserMaskProvider()
    {
      fProvider = null;

      EnsureProvider();
      if ( fProvider == null )
        return;

      UpdateProviderParameters();
    }

    private void UpdateProviderParameters()
    {
      EnsureProvider();
      if ( fProvider == null )
        return;

      fProvider.IncludeLiterals = IncludeLiterals;
      fProvider.IncludePrompt = IncludePrompt;
      fProvider.IsPassword = IsPassword;
      fProvider.PromptChar = PromptChar;
      fProvider.ResetOnPrompt = ResetOnPrompt;
      fProvider.ResetOnSpace = ResetOnSpace;
      fProvider.SkipLiterals = SkipLiterals;

      fProvider.Set( Text );
      RefreshText( SelectionStart );
    }

    #endregion

    #region ... mask helpers ...

    private bool IsValidKey( Key key, int position )
    {
      var virtualKey = (char) KeyInterop.VirtualKeyFromKey( key );
      MaskedTextResultHint resultHint;

      return Provider.VerifyChar( virtualKey, position, out resultHint );
    }

    private void RemoveChar( int position )
    {
      if ( Provider.RemoveAt( position ) )
        RefreshText( position );
    }

    private void RefreshText( int position )
    {
      Text = !IsFocused ? Provider.ToString( false, true ) : Provider.ToDisplayString();
      SelectionStart = position;
    }

    private void RemoveRange( int position, int selectionLength )
    {
      if ( Provider.RemoveAt( position, position + selectionLength - 1 ) )
        RefreshText( position );
    }

    private int GetEditPositionFrom( int startPosition )
    {
      var position = Provider.FindEditPositionFrom( startPosition, true );
      return position == -1 ? startPosition : position;
    }

    private int GetEditPositionTo( int endPosition )
    {
      while ( endPosition >= 0 && !Provider.IsEditPosition( endPosition ) )
        endPosition--;

      return endPosition;
    }

    private int UpdateText( string text, int position )
    {
      var textLegth = string.IsNullOrEmpty( Text ) ? 0 : Text.Length;

      if ( position < textLegth )
      {
        position = GetEditPositionFrom( position );

        if ( ( Keyboard.IsKeyToggled( Key.Insert ) && Provider.Replace( text, position ) ) || Provider.InsertAt( text, position ) )
          position++;

        position = GetEditPositionFrom( position );
      }

      RefreshText( position );

      return position;
    }

    #endregion

    #region  ... autoselect ...

    public static readonly DependencyProperty AutoSelectOnFocusProperty =
      DependencyProperty.Register( "AutoSelectOnFocus",
        typeof( bool ), typeof( TextBox ) );

    public bool AutoSelectOnFocus
    {
      get { return (bool) GetValue( AutoSelectOnFocusProperty ); }
      set { SetValue( AutoSelectOnFocusProperty, value ); }
    }

    protected override void OnPreviewGotKeyboardFocus( KeyboardFocusChangedEventArgs args )
    {
      base.OnPreviewGotKeyboardFocus( args );

      if ( !AutoSelectOnFocus )
        return;

      if ( !VisualTree.IsDescendantOf( args.OldFocus as DependencyObject, this ) )
        SelectAll();
    }

    protected override void OnPreviewMouseLeftButtonDown( MouseButtonEventArgs args )
    {
      base.OnPreviewMouseLeftButtonDown( args );

      if ( !AutoSelectOnFocus )
        return;

      if ( IsKeyboardFocusWithin )
        return;

      Focus();
      args.Handled = true;
    }

    #endregion

    private object CoerceTextCallback( object baseValue )
    {
      if ( baseValue == null )
        return null;

      if ( !HasProvider )
        return baseValue;

      Provider.Set( baseValue.ToString() );
      return Provider.ToDisplayString();
    }

    private void TextChangedCallback()
    {
      if ( !HasProvider || fSuppressor.Suppressed )
        return;

      var prov = Provider;
      var text = Text;

      if ( string.IsNullOrEmpty( text ) )
        return;

      using ( fSuppressor.Suppress() )
      {
        if ( !text.Equals( prov.ToDisplayString() ) )
          prov.Set( text );
      }
    }

    protected override void OnTextChanged( TextChangedEventArgs args )
    {
      base.OnTextChanged( args );

      if ( !AutoMoveFocus )
        return;

      if ( Text.Length == 0 || Text.Length != MaxLength || CaretIndex != MaxLength )
        return;

      if ( !this.CanMoveFocus( FocusNavigationDirection.Right, true ) )
        return;

      var direction = ( FlowDirection == FlowDirection.LeftToRight ) ? FocusNavigationDirection.Right : FocusNavigationDirection.Left;
      MoveFocus( new TraversalRequest( direction ) );
    }

    protected override void OnPreviewKeyDown( KeyEventArgs arg )
    {
      if ( AutoMoveFocus )
      {
        this.TryMoveFocus( arg );
        if ( arg.Handled )
        {
          base.OnPreviewKeyDown( arg );
          return;
        }
      }

      if ( !HasProvider )
      {
        base.OnPreviewKeyDown( arg );
        return;
      }

      var position = SelectionStart;
      var selectionLength = SelectionLength;

      switch ( arg.Key )
      {
      case Key.Back:
        if ( selectionLength == 0 )
          RemoveChar( GetEditPositionTo( --position ) );
        else
          RemoveRange( position, selectionLength );

        arg.Handled = true;
        break;

      case Key.Delete:
        if ( selectionLength == 0 )
          RemoveChar( GetEditPositionFrom( position ) );
        else
          RemoveRange( position, selectionLength );

        arg.Handled = true;
        break;

      case Key.Space:
        if ( selectionLength != 0 && IsValidKey( arg.Key, position ) )
          RemoveRange( position, selectionLength );
        else
          UpdateText( " ", position );

        arg.Handled = true;
        break;

      default:
        if ( selectionLength != 0 && IsValidKey( arg.Key, position ) )
          RemoveRange( position, selectionLength );

        arg.Handled = true;
        break;
      }

      base.OnPreviewKeyDown( arg );
    }

    protected override void OnPreviewTextInput( TextCompositionEventArgs arg )
    {
      if ( IsReadOnly || !HasProvider )
        return;

      arg.Handled = true;
      var position = SelectionStart;
      position = UpdateText( arg.Text, position );
      base.OnPreviewTextInput( arg );
    }
  }
}
